
package gameserver;

import common.PhysicsWorld;
import common.State;
import common.packet.BodyState;
import common.packet.ClientIn;
import common.packet.ClientInfo;
import common.packet.ClientOut;
import common.packet.GameState;
import common.packet.WorldState;
import common.packet.Packet;
import common.packet.PacketWithCode;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;

/**
 * Contains everything related to the game itself, specially for the match. It
 * owns the physics world, the client list, game state and configuration, etc.
 * Its like a singleton wannabe.
 *
 * @author xissburg
 */
public class Game
{
    private final Map<String, ClientThread> clients;
    private State state;
    private int lTeamPoints, rTeamPoints;
    private int time;//match time in milliseconds

    private final PhysicsThreadS physicsThread;
    private final PingRunnable pingRunnable;
    private Thread updateThread;

    public Game()
    {
        clients = new ConcurrentHashMap<String, ClientThread>();
        state = State.WAITING;
        lTeamPoints = rTeamPoints = 0;
        time = 0;

        physicsThread = new PhysicsThreadS(this);
        pingRunnable = new PingRunnable();

        new Thread(pingRunnable).start();//Bad practice: start thread in constructor
    }

    /**
     * Returns a GameState packet containing data about the current state of the game
     */
    public GameState getGameState()
    {
        Collection<ClientThread> clientsCopy = getClients();
        Collection<ClientInfo> cInfo = new ArrayList<ClientInfo>(clientsCopy.size());

        for(ClientThread ct: clientsCopy)
            cInfo.add(ct.getClient().getClientInfo());

        GameState gs = new GameState();
        gs.setClients(cInfo);

        gs.setState(state);

        gs.setTime(time);
        gs.setlTeamPoints(lTeamPoints);
        gs.setrTeamPoints(rTeamPoints);

        return gs;
    }

    public State getState() {
        return state;
    }

    /**
     * Effectively adds a new client to the game. It also notifies all other clients
     * in the clients map about the newcomer.
     *
     * @param ct The client to be added.
     */
    public void addClientThread(ClientThread ct)
    {
        synchronized(clients)
        {
            if (clients.isEmpty()) {
                //There aren't any others, then this one is the owner
                ct.getClient().getClientInfo().setOwner(true);
            } else {
                //Notify all other clients
                ClientIn cIn = new ClientIn(ct.getClient().getClientInfo());
                Collection<ClientThread> clientCol = getClients();
                for (ClientThread c : clientCol) {
                    try {
                        c.getClient().write(cIn);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }

            //make sure the new client is a spectator
            ct.getClient().getClientInfo().setType(ClientInfo.Type.SPECTATOR);
            clients.put(ct.getClient().getClientInfo().getName(), ct);
        }
    }

    public void removeClientThread(String clientName)
    {
        physicsThread.getPhysicsWorld().removeBody(clientName);
        ClientThread client = clients.get(clientName);
        clients.remove(clientName);

        //If there aren't anymore players stop the game if running.
        if(clients.isEmpty())
            stop();
        else
        {
            //Notify all clients
            ClientOut cOut = new ClientOut(client.getClient().getClientInfo().getName());
            Collection<ClientThread> clients_ = getClients();
            for(ClientThread ct: clients_)
                try {
                    ct.getClient().write(cOut);
                } catch (IOException ex) {}
        }
    }

    public void setClientInfo(ClientInfo ci)
    {
        String name = ci.getName();
        ClientInfo clientInfo = clients.get(ci.getName()).getClient().getClientInfo();
        boolean changed = false;

        //if it was a player and continues to be a player, just update its position
        if(clientInfo.getType() == ClientInfo.Type.PLAYER &&
           ci.getType() == ClientInfo.Type.PLAYER)
        {
            changed = true;
            float x = ci.getStartX();
            float y = ci.getStartY();
            clientInfo.setStartXY(x, y);
            clientInfo.setTeam(clientInfo.getStartX() < 0f?
                ClientInfo.Team.LEFT: ClientInfo.Team.RIGHT);
            physicsThread.getPhysicsWorld().setButtonState(name, x, y, ci.getTeam());
        }
        //if it was a player and now it is a spectator, remove from physicsWorld
        //and graphicsWorld
        else if(clientInfo.getType() == ClientInfo.Type.PLAYER &&
                ci.getType() == ClientInfo.Type.SPECTATOR)
        {
            changed = true;
            clientInfo.setType(ClientInfo.Type.SPECTATOR);
            physicsThread.getPhysicsWorld().removeBody(name);
        }
        //if it was a spectator and now it is a player, add it to the physicsWorld
        //and change its state in the physicsWorld and update its starting position
        else if(clientInfo.getType() == ClientInfo.Type.SPECTATOR &&
                ci.getType() == ClientInfo.Type.PLAYER)
        {
            changed = true;
            clientInfo.setType(ClientInfo.Type.PLAYER);
            float x = ci.getStartX(), y = ci.getStartY();
            clientInfo.setStartXY(x, y);
            clientInfo.setTeam(clientInfo.getStartX() < 0f?
                ClientInfo.Team.LEFT: ClientInfo.Team.RIGHT);
            physicsThread.getPhysicsWorld().addButton(name, x, y, clientInfo.getTeam());
        }
        //else if it was a spectator and wants to be a spectator there's nothing
        //to do.

        //Notify all other clients about any changes
        if(changed)
        {
            Collection<ClientThread> clients_ = getClients();
            ci.set(clientInfo);

            for(ClientThread ct: clients_)
            {
                try {
                    ct.getClient().write(ci);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * Returns an array (independent of the client map for thread safety) containing
     * the ClientThread's instances of all clients currently connected. ClientThread
     * is thread safe.
     *
     * @return An array of ClientThreads.
     */
    
    public Collection<ClientThread> getClients() {
        return new java.util.ArrayList<ClientThread>(clients.values());
    }

    /**
     * Starts physics and update thread. Asynchronous call, returns immediately.
     */
    public void start()
    {
        Collection<ClientThread> clientsCopy = getClients();

        state = State.RUNNING;
        new Thread(physicsThread, "Physics Thread").start();
        updateThread = new Thread(new UpdateRunnable(), "Update Thread");
        updateThread.start();

        //Notify all clients about the game start
        Packet p = new PacketWithCode(Packet.Code.START_GAME);
        for (ClientThread ct : clientsCopy) {
            try {
                ct.getClient().write(p);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    public void reset()
    {
        PhysicsWorld world = physicsThread.getPhysicsWorld();
        world.resetBall();

        Collection<ClientThread> clients_ = getClients();

        for(ClientThread ct: clients_)
        {
            ClientInfo ci = ct.getClient().getClientInfo();
            world.setButtonState(ci.getName(), ci.getStartX(), ci.getStartY(), ci.getTeam());
            Body b = world.getBody(ci.getName());
            b.setLinearVelocity(new Vec2(0,0));
            b.setAngularVelocity(0);
        }
    }

    public void stop()
    {
        state = State.WAITING;
        physicsThread.stop();
        reset();

        if(updateThread != null)
            updateThread.interrupt();
    }

    /**
     * Update thread code. It is responsible for sending game updates during the
     * match to all clients.
     * Note: to stop a thread running this runnable, call interrupt() in the
     * thread object.
     */
    private class UpdateRunnable implements Runnable
    {
        private volatile boolean running;
        private int stepsPerSecond;

        public UpdateRunnable() {
            running = true;
            stepsPerSecond = 20;
        }

        public void setStepsPerSecond(int stepsPerSecond) {
            this.stepsPerSecond = stepsPerSecond;
        }

        public void run()
        {
            running = true;
            
            while(running)
            {
                Collection<BodyState> out = null;

                try {
                    out = physicsThread.getPhysicsOutput(true);
                } catch (InterruptedException ex) {
                    //If interrupted, stop this thread
                    running = false;
                    break;
                }

                WorldState worldState = new WorldState(out);
                Collection<ClientThread> clients = getClients();

                for(ClientThread ct: clients)
                {
                    try {
                        ct.getClient().write(worldState);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
                
                try {
                    Thread.sleep(1000/stepsPerSecond);
                } catch (InterruptedException ex) {}
            }
        }
    }

    private class PingRunnable implements Runnable
    {
        private volatile boolean running;
        private long delay;

        public PingRunnable() {
            this(3000);
        }
        
        public PingRunnable(long delay) {
            running = true;
            this.delay = delay;
        }

        public void setDelay(long delay) {
            this.delay = delay;
        }

        public void stop()
        {
            running = false;
        }

        public void run()
        {
            running = true;

            while(running)
            {
                Collection<ClientThread> clients = getClients();

                for(ClientThread ct: clients)
                    ct.refreshLatency();

                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ex) {}
            }
        }
    }
}
